TextBox mejorado en .NET.

Cuando desarrollamos en .NET es habitual utilizar controles personalizados que mejoran los preexistentes. Habitualmente podremos hacer uso de librerías públicas que podemos obtener en internet (liberadas o de pago). Otras veces podremos crearnos nosotros mismos nuestras propias librerías de controles. Yo suelo hacer esto último cuando no se trata de algo demasiado complejo, ya que termino antes que buscando componentes de terceros y peleándome después con sus correspondientes licencias. Entre otras cosas porque cualquier tipo de licencia "share alike" no me sirve.

Uno de los controles que me ahorran mucho trabajo en la creación de formularios es este que presento hoy y que en su día llamé TextBoxFocused (en adelante TBF). Se trata de un TextBox que cambia de color al recibir y perder el foco (de ahí su nombre -por cierto, sé que debería ser FocusedTextBox, pero quería mantener lo de TextBox al inicio-) y permite controlar a priori y a posteriori qué se puede introducir en él.

Un formulario con algunos TBF se vería así:

Como se puede apreciar, sin tener que añadir código alguno al formulario, el TBF aparecerá sombreado cuando tenga el foco. Además cuando se esté introduciendo un texto se mostrará en rojo, y cuando ya esté validado quedará en verde, tal como se aprecia en las siguientes imágenes:

Nada realmente espectacular, pero queda bonito. Sin embargo en donde el TBF se muestra útil es en el control del contenido introducido. Este control se realiza de una doble manera: a priori y mediante una propiedad del control se establece qué tipo de introducciones se admitirán, a posteriori -si se ha seleccionado- se controla que lo que se ha introducido realmente fuera lo que se debía introducir, impidiendo abandonar el TBF en caso contrario.

Para ello, en la ventana de propiedades del control nos aparecen dos nuevas creadas para la ocasión, tal como se muestra a continuación:

En las propiedades de cosecha propia siempre añado el prefijo "am" para localizarlas todas juntas. En este caso amAllow permite fijar qué introducciones se permitirán (cualquier cosa, nada de nada, sólo enteros positivos, dobles negativos, etc.) y amPostVerification decide si se aplicará verificación posterior o no.

Esto de amPostVerification tiene un sentido claro. Y es que podemos decirle al control que admita números dobles negativos y así sólo permitirá introducir números, el signo negativo y el signo decimal. Pero es evidente que con esos caracteres se pueden crear expresiones que no se correspondan con un número doble negativo (p.ej. -30-3.8..-25). Con este control a posteriori verificamos que efectivamente lo sea.

A nivel de código el control a posteriori se podría haber implementado a base de cástings y control de errores, pero no ha sido la opción que he escogido. Queda al antojo de cada cual modificar el código como le plazca.

Para su implementación en una aplicación, basta con añadir una clase TextBoxFocused.vb, copiar todo el código que se adjunta a continuación, compilar la aplicación y a partir de entonces tendremos el control disponible para añadirlo en cualquier formulario. O también se puede añadir a una librería de controles personalizados (que es lo que hago yo) y mantenerlo un poquito más organizado.

Código completo (de libre uso como todo lo que aparece publicado en este blog):

'--------------------------------------------------------------------
' Author:             Albert Mata (www.albertmata.net)
' Last time modified: 2008-11-13
' Description:        Acts like a standard TextBox but with some 
'                     better features like changing color when gets 
'                     or losts focus and controlling allowed 
'                     introductions.
'--------------------------------------------------------------------
Imports System.Drawing

Public Class TextBoxFocused
    Inherits System.Windows.Forms.TextBox

#Region "Constants"

    '----------------------------------------------------------------
    ' Constants for colors.
    '----------------------------------------------------------------
    Private COLOR_FOCUSED As Color = Color.FromArgb(255, 240, 157)
    Private COLOR_NON_FOCUSED As Color = Color.White
    Private COLOR_VALIDATED As Color = Color.Green
    Private COLOR_NON_VALIDATED As Color = Color.Red

    '----------------------------------------------------------------
    ' Constants for special keys.
    '----------------------------------------------------------------
    Private ASC_BACKSPACE As Integer = 8
    Private ASC_SUPPRESS As Integer = 127
    Private ASC_DASH As Integer = 45
    Private ASC_COMMA As Integer = 44

#End Region

#Region "Enumerations"

    '----------------------------------------------------------------
    ' Enumerations.
    '----------------------------------------------------------------
    Public Enum Introduction
        NothingAtAll
        PositiveInteger
        NegativeInteger
        PositiveDouble
        NegativeDouble
        OnlyLetter
        OnlyLetterOrDigit
        Password
        Everything
        EverythingNonCasing
    End Enum

#End Region

#Region "Attributes&Properties"

    '----------------------------------------------------------------
    ' Attributes.
    '----------------------------------------------------------------
    Private aAllow As Introduction = Introduction.Everything
    Private aPostVerification As Boolean = True

    '----------------------------------------------------------------
    ' Public properties to be shown in design time.
    '----------------------------------------------------------------
    Public Property amAllow() As Introduction
        Get
            Return Me.aAllow
        End Get
        Set(ByVal value As Introduction)
            Me.aAllow = value
        End Set
    End Property
    Public Property amPostVerification() As Boolean
        Get
            Return Me.aPostVerification
        End Get
        Set(ByVal value As Boolean)
            Me.aPostVerification = value
        End Set
    End Property

#End Region

#Region "GraphicalControls"

    '----------------------------------------------------------------
    ' Event Me.GotFocus.
    '----------------------------------------------------------------
    Private Sub TextBoxFocused_GotFocus(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.GotFocus
        MyBase.BackColor = COLOR_FOCUSED
        MyBase.ForeColor = COLOR_NON_VALIDATED
    End Sub

    '----------------------------------------------------------------
    ' Event Me.LostFocus.
    '----------------------------------------------------------------
    Private Sub TextBoxFocused_LostFocus(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.LostFocus
        If Me.CheckText() Or Not Me.aPostVerification Then
            MyBase.BackColor = COLOR_NON_FOCUSED
            MyBase.ForeColor = COLOR_VALIDATED
        Else
            Me.Focus()
        End If
    End Sub

    '----------------------------------------------------------------
    ' Event Me.KeyPress.
    '----------------------------------------------------------------
    Private Sub TextBoxFocused_KeyPress(ByVal sender As Object, _
    ByVal e As System.Windows.Forms.KeyPressEventArgs) _
    Handles Me.KeyPress
        Select Case Me.aAllow
            Case Introduction.NothingAtAll
                'Allowing no character.
                e.Handled = True
                If AscW(e.KeyChar) = ASC_BACKSPACE Then
                    MyBase.Text = ""
                End If
            Case Introduction.PositiveInteger
                'Allowing just digits.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                End If
            Case Introduction.NegativeInteger
                'Allowing just digits and '-' character.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS _
                And AscW(e.KeyChar) <> ASC_DASH Then
                    e.Handled = True
                End If
            Case Introduction.PositiveDouble
                'Allowing just digits and ',' character.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS _
                And AscW(e.KeyChar) <> ASC_COMMA Then
                    e.Handled = True
                End If
            Case Introduction.NegativeDouble
                'Allowing just digits and '-' and ',' characters.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS _
                And AscW(e.KeyChar) <> ASC_DASH _
                And AscW(e.KeyChar) <> ASC_COMMA Then
                    e.Handled = True
                End If
            Case Introduction.OnlyLetter
                'Allowing only letters.
                If Not Char.IsLetter(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                Else
                    e.KeyChar = Char.ToUpper(e.KeyChar)
                End If
            Case Introduction.OnlyLetterOrDigit
                'Allowing only letters and digits and upper casing 
                'letters.
                If Not Char.IsLetterOrDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                Else
                    e.KeyChar = Char.ToUpper(e.KeyChar)
                End If
            Case Introduction.Password
                'Allowing only letters and digits but not upper 
                'casing letters.
                If Not Char.IsLetterOrDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                End If
            Case Introduction.Everything
                'Allowing everything upper casing letters.
                e.KeyChar = Char.ToUpper(e.KeyChar)
            Case Introduction.EverythingNonCasing
                'Allowing everything not upper casing letters (so 
                'doing nothing).
        End Select
    End Sub

#End Region

#Region "PrivateMethods"

    '----------------------------------------------------------------
    ' Checks introduced text according to Me.amAllow property and 
    ' returns True only when introduced text respects desired 
    ' Me.amAllow property.
    '----------------------------------------------------------------
    Private Function CheckText() As Boolean
        Select Case Me.aAllow
            Case Introduction.NothingAtAll
                'Checking no character has been introduced.
                Return Me.Text = ""
            Case Introduction.PositiveInteger
                'Checking a right positive integer has been 
                'introduced.
                Return Me.IsPositiveInteger(Me.Text) Or Me.Text = ""
            Case Introduction.NegativeInteger
                'Checking a right negative integer has been 
                'introduced.
                Return Me.IsNegativeInteger(Me.Text) _
                    Or Me.IsPositiveInteger(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.PositiveDouble
                'Checking a right positive double has been 
                'introduced.
                Return Me.IsPositiveDouble(Me.Text) _
                    Or Me.IsPositiveInteger(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.NegativeDouble
                'Checking a right negative double has been 
                'introduced.
                Return Me.IsNegativeDouble(Me.Text) _
                    Or Me.IsPositiveDouble(Me.Text) _
                    Or Me.IsNegativeInteger(Me.Text) _
                    Or Me.IsPositiveInteger(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.OnlyLetter
                'Checking only letters have been introduced.
                Return Me.HasOnlyLetters(Me.Text) Or Me.Text = ""
            Case Introduction.OnlyLetterOrDigit, _
                 Introduction.Password
                'Allowing only letters and digits have been 
                'introduced.
                Return Me.HasOnlyLettersOrDigits(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.Everything, _
                 Introduction.EverythingNonCasing
                'Allowing everything.
                Return True
        End Select
    End Function

    '----------------------------------------------------------------
    ' Checks a right positive integer has been introduced.
    '----------------------------------------------------------------
    Private Function IsPositiveInteger(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C)
            End While
        Else
            R = False
        End If
        Return R
    End Function

    '----------------------------------------------------------------
    ' Checks a right negative integer has been introduced.
    '----------------------------------------------------------------
    Private Function IsNegativeInteger(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C) OrElse C = ChrW(ASC_DASH)
            End While
        Else
            R = False
        End If
        'Checking:
        '  - first character is a dash character,
        '  - length must be at least 2 characters.
        Return R And S.LastIndexOf(ChrW(ASC_DASH)) = 0 _
                 And S.Length >= 2
    End Function

    '----------------------------------------------------------------
    ' Checks a right positive double has been introduced.
    '----------------------------------------------------------------
    Private Function IsPositiveDouble(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C) OrElse C = ChrW(ASC_COMMA)
            End While
        Else
            R = False
        End If
        'Checking:
        '  - there's one comma character and it's not the first one,
        '  - there's exactly only one comma character,
        '  - the comma character it's not the last one.
        Return R And S.IndexOf(ChrW(ASC_COMMA)) > 0 _
                 And S.IndexOf(ChrW(ASC_COMMA)) = _
                     S.LastIndexOf(ChrW(ASC_COMMA)) _
                 And S.LastIndexOf(ChrW(ASC_COMMA)) < S.Length - 1
    End Function

    '----------------------------------------------------------------
    ' Checks a right negative double has been introduced.
    '----------------------------------------------------------------
    Private Function IsNegativeDouble(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C) OrElse C = ChrW(ASC_DASH) _
                                    OrElse C = ChrW(ASC_COMMA)
            End While
        Else
            R = False
        End If
        'Checking:
        '  - first character is a dash character,
        '  - length must be at least 4 characters,
        '  - there's one comma character and it's not the first or 
        '    second one (-0,0),
        '  - there's exactly only one comma character,
        '  - the comma character it's not the last one.
        Return R And S.LastIndexOf(ChrW(ASC_DASH)) = 0 _
                 And S.Length >= 4 _
                 And S.IndexOf(ChrW(ASC_COMMA)) > 1 _
                 And S.IndexOf(ChrW(ASC_COMMA)) = _
                     S.LastIndexOf(ChrW(ASC_COMMA)) _
                 And S.LastIndexOf(ChrW(ASC_COMMA)) < S.Length - 1
    End Function

    '----------------------------------------------------------------
    ' Checks only letters have been introduced.
    '----------------------------------------------------------------
    Private Function HasOnlyLetters(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsLetter(C)
            End While
        Else
            R = False
        End If
        Return R
    End Function

    '----------------------------------------------------------------
    ' Checks only letters and digits have been introduced.
    '----------------------------------------------------------------
    Private Function HasOnlyLettersOrDigits(ByVal S As String) _
    As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsLetterOrDigit(C)
            End While
        Else
            R = False
        End If
        Return R
    End Function

#End Region

End Class

Tags: , , , , , ,

Leave a Reply