Ponga una clase en su vida

 

Fecha: 24/Ago/98 (18/Ago/98)
Autor: Luis Sanz, Hospital "Reina Sof�a" [email protected]


Visual Basic incorpor�, en su versi�n 4, un nuevo tipo de archivo: las clases. Pero �Qu� son? �Para qu� sirven?

�Qu� son las clases?

Realmente, cualquier objeto de Visual Basic es realmente una clase. Una clase es una "cosa" que tiene sus propios m�todos, eventos y propiedades. Algunas de ellas tienen una interfaz gr�fica (como, por ejemplo, una caja de texto, o un formulario). Por lo tanto, Visual Basic ha tenido clases desde siempre. Pero desde la versi�n 4, se pueden usar clases definidas por el usuario: los m�dulos de clase.

Las clases de VB tienen m�todos, eventos y propiedades. Las clases "hechas" (como los formularios) los tienen por defecto (por ejemplo, el m�todo Show, la propiedad Visible, y el evento Click). A ellos les podemos a�adir los propios. Sin embargo, los m�dulos de clase, hasta que los "rellenemos", s�lo tienen dos eventos (Class_Initialize y Class_Terminate). Hay propiedades ocultas, como Name, o Instancing, pero en principio no podemos acceder a ellas salvo desde la ventana de propiedades.

�C�mo usarlos?

Las clases "predise�adas" de VB, como los formularios, tienen una instancia por defecto. Por ejemplo, si tenemos un proyecto con el formulario frmPrueba, podemos poner:

frmPrueba.Show

Lo que hacemos en realidad es referirnos a la instancia por defecto del objeto "frmPrueba". Si queremos, podemos declarar nosotros mismos la instancia (lo cual suele ser recomendable) pero no es imprescindible:

Dim Fprueba As New frmPrueba
FPrueba.Show

Sin embargo, las clases no tienen una instancia "por defecto": para acceder a ellas, debemos declararlas. Si en el ejemplo anterior tenemos una clase llamada clsPrueba, con un m�todo p�blico "Pitar"

Public Sub Pitar()
    Dim i As Integer

    For i = 0 To 10
	Beep
    Next
End Sub

Si ponemos en el c�digo:

Private Sub Form_Click
    clsPrueba.Pitar
End Sub

Obtendremos un error del tipo "variable no definida". Para evitarlo, hay que poner:

Private Sub Form_Click()
    Dim CPrueba As clsPrueba

    Set CPrueba = New clsPrueba

    CPrueba.Pitar

End Sub

�:

Private Sub Form_Click()
    Dim CPrueba As New clsPrueba

    CPrueba.Pitar

End Sub

La variable CPrueba que hemos declarado tendr�, como cualquier otra variable, un �mbito, y un "ciclo de vida". Podremos declararla como local (dentro de un procedimiento), de m�dulo (privada o p�blica), o global, igual que hacemos con cualquier otra variable.

Cuando creamos una instancia de la variable, se crea una nueva copia del objeto en memoria. En ese momento, las variables de dicho m�dulo se inicializan con sus valores por defecto (es decir: vac�as, o cero). Pero la instancia se crea en un momento u otro, seg�n como se haya declarado la variable. Si se declara:

Dim CPrueba As clsPrueba
Set CPrueba = New clsPrueba ‘Se crea la instancia

la instancia se crea cuando establecemos la referencia (con Set…). Debemos recordar establecer en el c�digo la nueva referencia. Si la declaramos

Dim CPrueba As New clsPrueba
CPrueba.Pitar ‘Se crea la instancia

la referencia se crea en tiempo de compilaci�n, y la instancia se crea la primera vez que llamamos a la clase (con un m�todo, una propiedad…). Si descargamos la clase (con Set CPrueba = Nothing) y volvemos a llamar a la clase, se crea una nueva instancia.

Entre uno u otro m�todo apenas hay diferencias de velocidad. El primer m�todo da m�s control sobre la creaci�n de instancias, pero debemos recordar que tenemos que crearlas. El segundo m�todo es m�s c�modo, ya que llamaremos a la clase cuando queramos, pero est� m�s expuesto a una programaci�n descuidada. Hay que tener en cuenta el c�digo que se ejecute en Class_Initialize, ya que nos puede interesar que se ejecute en un momento u otro. Habitualmente, y por comodidad, usar� el segundo sistema.

El m�dulo de clases se descarga:

  • Cuando la variable sale de �mbito (al finalizar el procedimiento o el m�dulo en que est� declarada, o al finalizar el programa).
     
  • Haciendo referencia a Nothing:
    Set CPrueba = Nothing

Vamos a mostrar como se cargan y descargan las clases (el listado puede verse en el proyecto PContar).

Vamos a incluir una nueva clase (clsContar), que lo que hace es indicarnos cuantas veces se ejecuta su m�todo p�blico (Sumar), para lo cual usa una variable privada. Como es una clase muy educada, nos indica cuando se carga y cuando se descarga (cuando se producen los eventos Initialize y Terminate).

'Clase clsContar
Option Explicit
Private iMNumero As Integer

Public Sub Sumar()

    MNumero = iMNumero + 1

    MsgBox "Se han sumado " & CStr(iMNumero)

End Sub


Private Sub Class_Initialize()
    MsgBox "Hola"
End Sub


Private Sub Class_Terminate()
    MsgBox "Adi�s"
End Sub

Ahora, llamaremos a la clase desde un formulario. Como la clase debe contar, no debe salir de �mbito al acabar el procedimiento: la declararemos en la secci�n "Declaraciones" del formulario:

Option Explicit
Private CContar As clsContar
Private Sub cmdContar_Click()
    CContar.Sumar
End Sub
Private Sub Form_Load()
    Set CContar = New clsContar
End Sub

Si ejecutamos el programa, veremos c�mo se crea la instancia de la clase en el evento Load del formulario, y se destruye tras el evento Unload de �ste. Cada vez que se pulsa el bot�n "Sumar", se suma uno.

Ahora, modificaremos el evento cmdContar_Click

Private Sub cmdContar_Click()
    CContar.Sumar

    Set CContar = Nothing
End Sub

La primera vez que se pulsa el bot�n, se ejecuta el m�todo sumar, y la instancia se despide y se destruye. La siguiente vez, se produce un error. Si hacemos de nuevo referencia a la clase:

Private Sub cmdContar_Click()

    CContar.Sumar

    Set CContar = Nothing

    Set CContar = New clsContar

End Sub

Veremos c�mo la clase se carga y se descarga, y el valor de las variables internas no se conserva.

Si ahora declaramos la variable de la otra forma (con As New):

Option Explicit
Private CContar As New clsContar
Private Sub cmdContar_Click()
    CContar.Sumar
End Sub

Veremos que el comportamiento es diferente. La instancia de la clase se crea la primera vez que se hace referencia a �sta. Si ahora eliminamos la referencia:

Private Sub cmdContar_Click()
    CContar.Sumar

    Set CContar = Nothing
End Sub

Veremos c�mo no se produce un error, sino que se crea una nueva instancia de la clase cada vez que se hace referencia a �sta, pero, al ser una nueva "copia", no se conserva el valor de las variables internas. Por ello hay que ser m�s cuidadoso si se usa el segundo m�todo de declarar las clases.

�C�mo a�adir c�digo?

Una clase puede tener m�todos, propiedades y eventos, p�blicos o privados. Los m�todos, etc�tera, privados son internos a la clase, y ahora no nos interesan. Los p�blicos constituyen la interfaz p�blica de la clase, es decir, lo que la clase "expone" al resto del programa. Para implementarlos, podemos a�adirlos manualmente, o usar el asistente de VB5 "Class Builder Utility". La ventaja del asistente es que es sencillo de usar, y evita errores. Pero la nomenclatura de las variables no es mu all�, que digamos.

A�adir un m�todo a una clase

Basta con declarar un procedimiento (Sub o Function) como Public:

Public Sub Sumar()

    iMNumero = iMNumero + 1

    MsgBox "Se han sumado " & CStr(iMNumero)

End Sub
Public Function Total() As Integer
    Total = iMNumero
End Function

Para acceder a ellas, se hace como para acceder a un m�todo p�blico de un formulario:

CContar.Sumar
I = CContar.Total

A�adir una propiedad a una clase

La forma m�s sencilla es a�adir una variable p�blica:

Public NumeroInicio As Integer

Y se accede:

If 0 = CContar.NumeroInicio Then CContar.NumeroInicio = 3

Pero, aunque funciona, es una forma peligrosa: los valores que establezcamos no estar�n "filtrados". Adem�s, en ocasiones querremos que se ejecutan acciones cuando se modifique un valor (igual que en un formulario cambia el color de fondo al modificar la propiedad BackColor). Para ello, usaremos los procedimientos Property Let y Property Get:

'En la clase clsContar

Private iMNumeroInicio As Integer
Public Property Let NumeroInicio(ByVal NuevoNumero As Integer)
    iMNumeroInicio = NuevoNumero
    iMNumero = iMNumeroInicio
End Property
Public Property Get NumeroInicio() As Integer
    NumeroInicio = iMNumeroInicio
End Property
'En frmPrueba

Private Sub txtContar_Change()
    CContar.NumeroInicio = Val(txtContar)
End Sub

S queremos establecer la referencia a un objeto, debemos hacerlo con Property Set:

Private FMVentana As Form
Public Property Set Ventana(ByVal NuevaVentana As Form)
    Set FMVentana = NuevaVentana
End Property
Public Property Get Ventana() As Form
    Set Ventana = FMVentana
End Property

 

A�adir eventos a una clase

Esta es una de las nuevas caracter�sticas de las clases que tiene VB5.

En ocasiones, querremos que la clase nos "avise" de que ha pasado algo, de la misma forma que una caja de texto nos avisa que ha cambiado su contenido. Es decir, que ha pasado algo: que ha ocurrido un evento. Pero implementar un nuevo evento no es tan sencillo como a�adir una propiedad:

En primer luger, debemos a�adir el evento a la clase:

Public Event CambioNumero(ByVal Numero As Integer)

Para disparar los eventos, debe usarse RaiseEvent

Public Sub Sumar()

    iMNumero = iMNumero + 1

    MsgBox "Se han sumado " & CStr(iMNumero)

    RaiseEvent CambioNumero(iMNumero)

End Sub

Ahora es preciso "recibir" los eventos. Eso m�s complejo. En primer lugar, debemos declarar la clase con la palabra clave WithEvents. No puede hacerse ni en un procedimiento (variable local) ni en un m�dulo, sino en la secci�n "Declaraciones" de un formulario (o de una clase). No puede usarse la palabra clave New

Private WithEvents CContarEventos As clsContar

Ahora, si en el visor del c�digo miramos en la ventana de controles, veremos que hay una nueva entrada: CContarEventos. Es como si hubi�semos a�adido un nuevo control al formulario. Este nuevo "control" s�lo tendr� disponible un evento por ahora (CambioNumero), pero podemos crear los que necesitemos.

Para recibir el evento, todav�a hemos de crear la instancia:

Private Sub Form_Load()
    Set CContarEventos = New clsContar
End Sub

Ahora podemos incluir c�digo en el nuevo evento.

Private Sub cmdEventos_Click()
    CContarEventos.Sumar
End Sub
Private Sub CContarEventos_CambioNumero(ByVal Numero As Integer)
    MsgBox "El nuevo n�mero es " & CStr(Numero)
End Sub

Como con cualquier otro procedimiento, podemos editar sus caracter�sticas. Para ello seleccionaremos en el men� de VB Herramientas|Atributos del procedimiento. Podemos seleccionar un procedimiento y cambiar sus atributos (pulsando "Avanzadas" podemos hacer que sea el procedimiento por defecto, que est� oculto, etc.

�Para qu� sirve una clase?

Con todo esto, ya sabemos crear una clase que funcione. Pero si una clase s�lo va a servir para adornar un programa, no va a tener mucha utilidad. En la programaci�n habitual �para qu� podemos usarlas?

Ante todo, las clases no son imprescindibles. Los programadores de BASIC de siempre, de Visual Basic (hasta la versi�n 4), etc�tera, se las han arreglado para hacer buenos programas sin complicarse con las clases. Pero facilitan mucho determinadas tareas.

Lo que viene luego no es una lista exclusiva. Las clases, al ser objetos hechos "a medida" pueden usarse para las cosas m�s estramb�ticas". S�lo voy a se�alar algunas de sus posibilidades m�s sencillas.

Procedimientos blindados (encapsular procedimientos)

Una de las principales ventajas de las clases es la encapsulaci�n. Cuando creamos una instancia de una clase, se crea un espacio de memoria reservado para esta, con sus propias variables, que no interfieren entre s�. Si creamos un m�dulo est�ndar, s�lo hay en memoria una "copia" de �ste, con un �nico "juego" de variables. Por ello es mucho m�s sencillo que unos procedimientos interfieran con los otros.

Para demostrarlo, vamos a tener un programa que nos va a indicar cuantas veces nos rascamos. Cada vez que lo hagamos, pulsaremos el bot�n correspondiente. Y tendremos dos ventanas que nos dir�n cuantas veces lo hemos hecho, pero una usar� un m�dulo est�ndar, y la otra una clase. Vamos a tener el mismo c�digo en el m�dulo y en la clase.

'frmSumar
Option Explicit
Private CSuma() As New clsSumar
Private Sub Form_Load()
    ReDim CSuma(cmdRascar.UBound)
End Sub
Private Sub cmdRascar_Click(Index As Integer)
    lblClase(Index).Caption = CStr(CSuma(Index).Sumar(1))
    lblModulo(Index).Caption = CStr(modSumar.Sumar(1))
End Sub
'clsSumar
Option Explicit
Private iMSuma As Integer
Public Function Sumar(Sumando As Integer) As Integer
    iMSuma = iMSuma + Sumando
    Sumar = iMSuma
End Function
'modSuma
Option Explicit
Private iMSuma As Integer
Public Function Sumar(Sumando As Integer) As Integer
    iMSuma = iMSuma + Sumando
    Sumar = iMSuma
End Function

En apariencia, el c�digo es el mismo. Y el funcionamiento, igual. Hasta ahora.

Pero como somos muy nerviosos, no s�lo nos rascamos la nariz, tambi�n lo hacemos con la oreja. Y vamos a aprovechar el proyecto anterior. Como �ramos previsores, ya hab�amos creado una matriz de controles. Ahora, simplemente a�adimos otro bot�n y dos ventanas. Dejamos el c�digo igual

�Qu� ha pasado? Ya no suman igual. Cuando sumamos con el m�dulo, la suma es incorrecta. La causa es que el m�dulo usa la misma variable para almacenar, mientras que cada clase usa su propia variable.

Esto podr�a haberse evitado con una programaci�n m�s cuidadosa del m�dulo est�ndar:

'en frmSumar
Private Sub cmdRascar_Click(Index As Integer)
    lblClase(Index).Caption = CStr(CSuma(Index).Sumar(1))
    lblModulo(Index).Caption = CStr(modSumar.Sumar(1, Index))
End Sub
'en modSuma
Option Explicit
Private iMSuma(1) As Integer
Public Function Sumar(Sumando As Integer, Indice As Integer) As Integer
    iMSuma(Indice) = iMSuma(Indice) + Sumando
    Sumar = iMSuma(Indice)
End Function

Ahora funciona bien. Pero a costa de tener que modificar el c�digo de frmSumar y de modSuma, que por lo visto est�n estrechamente relacionados.

Mientras tanto �que ha pasado con clsSumar? Pues nada. Como estaba bien dise�ada desde el principio, no ha requerido ninguna modificaci�n. Se usa una instancia para cada bot�n, y cada una de ellas tiene su propio juego de variables, que no interfieren entre s�. Los cambios de frmSumar no le han afectado, y ha funcionado bien las dos veces.

Resumiendo: en muchas ocasiones, el uso de clases permite escribir procedimientos m�s sencillos y menos dependientes del "medio externo".

C�digo perenne (reutilizar c�digo)

La ventaja del c�digo encapsulado es que es mucho m�s sencillo incluirlo en otros proyectos. En el ejemplo anterior, hemos visto como hab�a que modificar el m�dulo est�ndar para a�adir un bot�n. Pero �y si queremos a�adir otro m�s?

Ahora, supongamos que estamos acatarrados, y empezamos a estornudar

Si pulsamos "Estornudo", se produce un nuevo error. Hemos de modificar de muevo modSuma para que funcione. Tal como funcionan hasta ahora, frmSumar y modSuma, est�n interrelacionados, y un cambio en uno de ellos requiere una modificaci�n en el otro.

Desde luego, este problema se podr�a haber evitado. Se podr�a haber puesto un procedimiento especial, que modificase la dimensi�n de la matriz de variable "almac�n", que se llamase durante Form_Load, etc. Pero entonces nos estar�amos complicando la vida, y el formulario y el m�dulo seguir�an dependiendo uno del otro.

Sin embargo, clsSumar sigue funcionando igual de bien que antes. No hemos cambiado nada, y el c�digo es el mismo en los tres proyectos. Es m�s, podr�amos tener todas las copias que necesit�semos, funcionando a la vez, sin problemas. Incluso podemos cambiar su c�digo interno:

Option Explicit
Public Function Sumar(Sumando As Integer) As Integer
    Static iLStSuma As Integer 

    iLStSuma = iLStSuma + Sumando
    Sumar = iLStSuma
End Function

y sigue funcionando bien. Es porque su interfaz p�blica no se ha modificado. Esto es importante: si una clase est� bien dise�ada, mientras no se modifique su interfaz p�blica, podemos hacer todas las modificaciones internas que queramos, y el resto del programa no se afectar�.

M�s a�n: podemos incluir la misma clase en varios proyectos. No una copia del archivo .CLS en cada uno, sino el mismo archivo en varios proyectos. Funcionar� bien. Y si un d�a detectamos un error, o hacemos una mejora, al modificar la clase se actualizar�n todos los proyectos que la usen �qui�n se atreve a hacer lo mismo con un m�dulo?

Y todav�a m�s. Ni siquiera tenemos que escribir la clase nosotros. Podr�a ponerme de acuerdo con un compa�ero:

- "Nacho, necesito una clase que me vaya sumando de uno en uno"

- "No te preocupes, Luis, que te la hago. Tendr� un m�todo p�blico Sumar, que recibir� como par�metro el sumando, y devolver� el total"

- "Justo lo que necesitaba. Date prisa, por favor"

Ahora que estamos de acuerdo, Nacho puede escribir lo que quiera, ya que su c�digo se "adaptar�" al m�o. Pero esto trae consigo una exigencia: si queremos usar clases y reutilizarlas (o compartirlas), el c�digo debe estar encapsulado (nada de variables globales, por favor) y se debe mantener la interfaz p�blica sin modificaciones. De hecho, VB suministra herramientas para poder reutilizar la interfaz p�blica de las clases (es eso del polimorfismo; paciencia, ya llegaremos).

En resumen: usar clases, aunque requiere ser "disciplinado" al programar, acaba facilitando la vida al permitirnos reutilizar el c�digo, sobre todo si trabajamos en grupo.

Clases y controles

Como hemos visto hasta ahora, una de las caracter�sticas de las clases es la posibilidad de usar "copias m�ltiples" de ellas. Por eso no interfieren entre s�, al usar su propio "juego" de variables. Por ejemplo, si tenemos una clase clsCaja, con una variable p�blica "Texto", podemos poner:

Private cCaja1 As New clsCaja
Private cCaja2 As New clsCaja
Private cCaja3(1) As New clsCaja

Y luego:

cCaja1.Texto = "Carlos"
If cCaja2.Texto = "Pepe" Then ...
For i = 0 To UBound(cCaja3)
    cCaja3(i).Texto = CStr(i)
Next

Esto �no suena de nada? Si resulta que en el formulario tenemos cajas de texto Text1, Text2, Text3(0) y Text3(1), podr�amos haber escrito un c�digo muy similar. Se debe a que realmente, una caja de texto es una clase (como hemos visto antes). Text1, Text2, etc. son instancias de la clase TextBox. Y cada una de ellas tiene su propio "juego" de variables, para cada una de ellas. Y si una tiene la propiedad Text = "Carlos", y otra "Pepe", no nos devolver�n nunca valores "cambiados".

Pero �y si quisi�semos a�adir una propiedad a un control? Por ejemplo, queremos que la caja de texto que vamos a utilizar tenga una propiedad de s�lo lectura "Cambiado", que indique si el texto se ha modificado, y un m�todo "Actualizar", que "pone a cero" el valor de cambiado �C�mo podr�amos hacerlo?

La forma l�gica de hacerlo ser�a usar una de las nuevas posibilidades de VB5: hacer un control de usuario que herede las caracter�sticas de un TextBox, y a�adirle las propiedades y m�todos nuevos. Con ello conseguiremos una nueva caja "a medida". Pero es algo latosa de desarrollar, y consume bastante recursos. Y total, para una s�la propiedad... �Se puede hacer de otra forma?

Podr�a hacerse con un m�dulo est�ndar. Es f�cil hacerlo si en el formulario hay un solo control (ver el c�digo de Pcambio0):

'modCaja
Option Explicit
Private CajaCambiado As Boolean
Public Sub Actualizar(Cambio As Boolean)
    CajaCambiado = Cambio
End Sub
Public Function Cambiado(hwnd As Long) As Boolean
    Cambiado = CajaCambiado
End Function
'frmCambio
Option Explicit
Private Sub Text1_Change()
    Releer
End Sub
Private Sub Releer()
    Dim i As Integer
    For i = 0 To UBound(cText)
	If cText(i).Cambiado Then
	    lblCambio(i).Caption = "Cambiado"
	Else
	    lblCambio(i).Caption = "No cambiado"
	End If
    Next
End Sub

Pero, como es evidente, esto no funcionar� con varios controles, ya que interferir�n con el valor de CajaCambiado en el m�dulo, al haber una s�la copia del procedimiento. Necesitamos alg�n "marcador" que nos indique que caja es. Para eso usaremos la propiedad hwnd (es el manejador de la ventana, o sea, la forma que Windows tiene de identificar la "instancia" de la ventana, que ser� �nica para esta). Luego, en el m�dulo est�ndar, guardaremos el valor del manejador, y si se ha cambiado. Es necesario a�adir un procedimiento para "incluir" nuevos controles (ver el c�digo de Pcambio):

 

 

'frmCambio
Option Explicit
Private Sub Text1_Change(Index As Integer)
    Actualizar Text1.hwnd, True
    Releer
End Sub
Private Sub Text2_Change()
    Actualizar Text2.hwnd, True
    Releer
End Sub
Private Sub Releer()
    Dim C As Control
    Dim i As Integer

    For Each C In Controls
	If TypeOf C Is TextBox Then
	    If Cambiado(C.hwnd) Then
		lblCambio(3 - i).Caption = "Cambiado"
	    Else
		lblCambio(3 - i).Caption = "No cambiado"
	    End If
	    i = i + 1
	End If
    Next
End Sub
'modCaja
Option Explicit
Private Cajahwnd() As Long
Private CajaCambiado() As Boolean

Public Sub Actualizar(hwnd As Long, Cambio As Boolean)
    Dim iLMax As Integer
    Dim bLHay As Boolean
    Dim i As Integer

    On Error Resume Next

    iLMax = UBound(Cajahwnd)
    If Err = 0 Then
	For i = 0 To iLMax
	    If Cajahwnd(i) = hwnd Then
		CajaCambiado(i) = Cambio
		bLHay = True
	    End If
	Next
    End If

    If Not bLHay Or Err Then A�adir hwnd, Cambio
End Sub
Public Function Cambiado(hwnd As Long) As Boolean
    Dim iLMax As Integer
    Dim bLHay As Boolean
    Dim i As Integer

    On Error Resume Next

    iLMax = UBound(Cajahwnd)
    If Err = 0 Then
	For i = 0 To iLMax
	    If Cajahwnd(i) = hwnd Then
		Cambiado = CajaCambiado(i)
		bLHay = True
	    End If
	Next
    End If

    If Not bLHay Or Err Then A�adir hwnd
End Function
Private Sub A�adir(hwnd As Long, Optional Cambio As Boolean)
    Dim iLMax As Integer

    On Error Resume Next

    iLMax = UBound(Cajahwnd)
    If Err Then iLMax = -1
    ReDim Preserve Cajahwnd(iLMax + 1)
    ReDim Preserve CajaCambiado(iLMax + 1)
    Cajahwnd(iLMax + 1) = hwnd
    CajaCambiado(iLMax + 1) = Cambio
End Sub

Con todo esto, funciona, independientemente del n�mero de controles que incorporemos. Pero a base de "chapuzas": no es ni el�stico, ni robusto. Vamos a ver problemas:

  • No se ha inclu�do ninguna rutina para "eliminar" controles: cada vez que descargamos un formulario, se guardan los controles "antiguos" (y si se recarga el formulario, como variar� el hwnd, no se reutilizar�n).
  • No podemos acceder a valores antiguos "guardados", ya que no podemos encontrarlos (el hwnd habr� variado).
  • Si los controles tienen que interactuar entre ellos, puede ser necesaria una rutina que indique que "cambiado" no debe utilizarse. Con este sistema, es muy dif�cil implementarlo.
  • Para leer los valores de "Cambiado", es necesario recorrer la colecci�n Controls del formulario (y autom�ticamente a�ade los controles no usados, que tal vez no interesen).

En resumen: funciona, pero a base de un c�digo complejo, dif�cil de adaptar, y con un mayor consumo de recursos. Tal vez sea m�s sencillo hacerlo con clases. Veremos como queda el c�digo (Pcambio2):

'clsCaja
Option Explicit
Public Caja As Object
Private bMCambiado As Boolean 'local copy

Public Sub Actualizar(Cambio As Boolean)
    bMCambiado = Cambio
End Sub

Public Property Get Cambiado() As Boolean
    Cambiado = bMCambiado
End Property
'frmCambio
Option Explicit
Private cText(3) As New clsCaja

Private Sub Form_Load()
    Set cText(0).Caja = Text1(0)
    Set cText(1).Caja = Text1(1)
    Set cText(2).Caja = Text1(2)
    Set cText(3).Caja = Text2
End Sub
Private Sub Text1_Change(Index As Integer)
    cText(Index).Change
    Releer
End Sub
Private Sub Text2_Change()
    cText(3).Change
    Releer
End Sub
Private Sub Releer()
    Dim i As Integer

    For i = 0 To UBound(cText)
	If cText(i).Cambiado Then
	    lblCambio(i).Caption = "Cambiado"
	Else
	    lblCambio(i).Caption = "No cambiado"
	End If
    Next
End Sub

El c�digo es mucho m�s sencillo usando clases. Tanto dentro de la misma clase (como el acceso es "�nico", nos olvidamos de matrices, etc.) como en el formulario (es m�s sencillo recorrer la matriz de clases). Pero hay otras ventajas:

  • Cada instancia de la clase clsCaja se destruye al salir del formulario, y evitamos un gasto in�til de recursos.
  • Es mucho m�s f�cil mejorar el c�digo de la clase. Por ejemplo: podemos a�adir el procedimiento Property Let para la propiedad de s�lo lectura (para que nos muestre un error interceptable, en lugar de un error al compilar). Tambi�n podemos a�adir nuevos m�todos, etc. Como no var�an los antiguos, no afectan al programa. Tambi�n podr�a haberse hecho con el m�dulo, pero con mayor dificultad.
Public Property Let Cambiado(ByVal Cambio As Boolean)
    Err.Raise 12001, , "Esta propiedad es s�lo lectura"
End Property

En el ejemplo, de todas formas, hemos podido substituir la clase por un m�dulo, con un poco de trabajo. Pero no siempre es tan sencillo: podr�amos tener procedimientos que se llamasen recursivamente, o que desde unos controles se act�e en otros. En esos casos, es m�s f�cil controlar el valor de las variables si hay una copia para cada control, que si tenemos que identificar cada control para evitar que uno interfiera en la �nica copia de la variable..

En resumen: es mucho m�s sencillo "asociar" un procedimiento a un control si est� en un m�dulo de clases que si est� en un m�dulo est�ndar, ya que podremos tener una instancia para cada control, y no habr� riesgo de interferencias entre las variables.

Variables con filtro (clases en lugar de variables)

Algunas variables s�lo deber�an admitir valores en unos rangos determinados. Por ejemplo: supongamos que en un programa necesitamos guardar la edad de una amiga. Para ello, tenemos una variable "EdadPersonaA�os". Para esta variable ser�a aceptable un valor entre 0 y 120, pero no lo ser�an valores negativos ni enormes. Inadvertidamente podr�amos incluir un valor absurdo �C�mo evitarlo? Desde luego, es f�cil hacer comprobaciones en el c�digo, pero �no ser�a mejor que fuese la variable la que se encargase de ello? Para eso podemos usar una clase, la clase clsA�os:

En el formulario pondremos:

Option Explicit
Private CA�os As New clsA�os
Private Sub Form_Load()
    txtA�os.Text = CA�os.EdadPersonaA�os
End Sub
Private Sub txtA�os_Change()
    CA�os.EdadPersonaA�os = Val(txtA�os)
End Sub 

Y en la clase clsA�os

Option Explicit
Private iMEdadPersonaA�os As Integer
Private Const ctMEdadMinima As Integer = 0
Private Const ctMEdadMaxima As Integer = 120
Private Const ctMEdadDefecto As Integer = 27

Public Property Let EdadPersonaA�os(ByVal Edad As Integer)
    If Edad < ctMEdadMinima Or Edad > ctMEdadMaxima Then
	Err.Raise 11111, "clsA�os", _
		"�Seguro que la edad es correcta?"
    Else
	iMEdadPersonaA�os = Edad
    End If
End Property
Public Property Get EdadPersonaA�os() As Integer
    EdadPersonaA�os = iMEdadPersonaA�os
End Property
Private Sub Class_Initialize()
    iMEdadPersonaA�os = ctMEdadDefecto
End Sub 

Ahora tenemos, en lugar de una variable "tonta" tenemos una clase que filtra los datos introducidos, y que incluso se inicializa con un valor por defecto. Ello tiene una segunda ventaja, menos evidente, y es que facilita el mantenimiento del c�digo. Si de repente queremos ser estricto, y no queremos saber nada de ni�as ni de viejas, es muy sencillo cambiar los l�mites sin tocar el c�digo. Basta por modificar los l�mites:

Private Const ctMEdadMinima As Integer = 18
Private Const ctMEdadMaxima As Integer = 40

En plan chulo, podr�amos haber puesto los l�mites y el valor por defecto, no como constantes, sino como propiedades. En ese caso podr�amos reutilizar la clase: para nuestras amigas, para un colegio, para un programa de gesti�n de empleados… (ver el c�digo de Pa�os).

Por desgracia, no es oro todo lo que reluce. Las clases son lentas y consumen recursos. En el ejemplo anterior, usando la funci�n del API GetTickCount (en las p�ginas del Guille encontrareis un ejemplo) para comparar la velocidad de modificar el valor de una variable Long, o de la clase clsA�os, se puede ver que esta �ltima es unas 100 veces m�s lenta (no os asusteis; aun as� es rapid�sima), y el consumo de recursos mucho mayor. Y tampoco aconsejar�a a nadie empezar a sustituir todas sus variables por clases. Ni lo recomendar�a para operaciones donde la velocidad es cr�tica (como las operaciones gr�ficas). Ahora bien, cuando la seguridad sea cr�tica (por ejemplo, en los valores que se guardar�n en una base de datos), es una opci�n.

De todas formas, no parece muy eficiente usar un mont�n de clases en cada proyeco �se podr�an agrupar?

Usar Clases en lugar de Tipos Definidos por el Usuario

Los tipos definidos por el usuario (TDU) son un tipo especial de variables "agrupadas" (no es realmente as�; es como si fuese una variable "dividida en trozos", pero para el caso nos vale). En ocasiones pueden resultar muy �tiles. Por ejemplo, en el caso anterior: supongamos que estamos haciendo un programa que lleva la gesti�n de nuestros amigos, y tenemos varios datos para cada uno de ellos: el nombre y los apellidos, la edad y el sexo (lo realmente importante). Podr�amos hacerlo as�:

Public Nombre() As String
Public Apellido1() As String
Public Apellido2() As String
Public Edad() As Integer
Public SexoVaron() As Boolean

Pero el problema con este sistema es que va a ser preciso redimensionar cada matriz (con el consiguiente riesgo de errores). Por ello, es mucho m�s sencillo hacerlo as�:

'M�dulo est�ndar modAmigos
Option Explicit
Public Type TipoAmigo
    Nombre As String
    Apellido1 As String
    Apellido2 As String
    Edad As Integer
    SexoVaron As Boolean
End Type
Public tGAmigo() As TipoAmigo

Ahora hemos creado una matriz de un TDU propio. Ahora, si tenemos veinte amigos, ponemos:

Redim tGAmigo (20)

Y si queremos a�adir un amigo m�s:

Redim Preserve tGAmigo (UBound(tGAmigo) + 1)

Luego, para almacenar los datos, podemos usar un archivo de acceso aleatorio (ver el curso b�sico del Guille) o una base de datos.

Pero los TDUs tienen sus limitaciones. Una es evidente: ni filtran los datos ni operan con ellos. Por eso, en el ejemplo anterior, podr�amos tener un problema con la edad: habr� que modificarla manualmente cada a�o. Tal vez sea mejor usar alg�n otro sistema, como veremos luego.

Pero otra limitaci�n es menos evidente: no podemos usarlos como miembros p�blicos de un formulario, ni pasarlos como argumento de un m�todo o funci�n p�blico, ni tener una funci�n que nos devuelva un TDU. Y eso puede ser fuente de problemas:

En el ejemplo anterior, queremos que el programa que estamos haciendo tenga una pantalla para editar los datos del amigo. Y queremos usar la misma pantalla tanto para introducir un amigo nuevo, como para modificar los datos de una amigo anterior �C�mo hacerlo? Podr�amos tener un m�todo p�blico en frmAmigo que tuviese como par�metro TipoAmigo:

Public Sub EditarAmigo(ByRef Amigo As TipoAmigo)

Pero nos da un error: no se pueden usar tipos definidos por el usuario como miembros p�blicos de un m�dulo de objeto. �C�mo lo pasaremos, pues? Una soluci�n ser�a tener una copia provisional "externa" de TipoAmigo, y un formulario escribe en una y el otro lo lee. Necesitaremos tambi�n alguna variable que indique si se han hecho cambios, etc. Para ello, declaramos en un m�dulo est�ndar:

Public tGAmigoProv As TipoAmigo

El proyecto PAmig0.vbp est� desarrollado con TDUs. Aparentemente funciona, pero tiene varias deficiencias:

  • Requiere la utilizaci�n de varias variables p�blicas (una variable TipoAmigo, y dos variables booleanas "indicadoras de estado". Aunque funcione, no es una buena pr�ctica de programaci�n: �Qu� pasar�a si tuvi�semos ocho formularios de introducci�n de datos? Cada uno de ellos requerir�a sus propias variables p�blicas, y si por error un formulario modifica una variable que no sea la suya (por un error de nomenclatura), o nos olvidamos de "poner a cero" alg�n indicador, se producir�an errores muy dif�ciles de depurar.
  • La matriz de TDUs d�nde se almacenan los datos en memoria, al ser p�blica, es accesible desde cualquier punto del programa. Y podemos modificarla por error. Por ejemplo, podr�amos haber hecho una rutina que comprobase que las edades fuesen correctas. Pero podemos acceder a la matriz de TDUs sin pasar por esa rutina. Esos "atajos" son una fuente potencial de errores.
  • No se filtran los valores introducidos. La validaci�n de los datos debe hacerse a nivel de formulario, con todos los problemas que ello implica (ver "Variables con l�mites").

Otra posibilidad ser�a usar, en lugar de variables p�blicas, un m�todo p�blico en el formulario, cada uno de cuyos par�metros fuese uno de los "componentes" de TipoAmigo:

Public Function EditarAmigo(Nombre As String, Apellido1 As String, ...

Tambi�n funcionar�a. Pero implicar�a una relaci�n demasiado estrecha (incestuosa, vamos) entre TipoAmigo, frmGestion y frmAmigo. Por ejemplo, si modificamos el componente "Edad" a Long, hay que modificar los m�todos p�blicos, etc. Si se a�ade otro "Componente" (Domicilio), lo mismo.

Todos estos problemas se pueden evitar mediante una programaci�n cuidadosa. Pero requiere tiempo y, si encima se est� desarrollando en un grupo, obliga a que los participantes est�n continuamente en contacto, ya que las modificaciones del c�digo de uno afectan a los dem�s (por ejemplo: si se usan variables p�blicas, hay que ponerse de acuerdo con el nombre de las variables, para que no interfieran).

Para evitar todos estos problemas, es conveniente usar otro estilo de programaci�n, lo m�s modular posible, y que evite el uso de variables p�blicas. Lo ideal ser�a que TipoAmigo tuviese sus propias reglas de validaci�n, sus valores por defecto, procesase la edad... Y que pudiese pasarse como un par�metro privado, ya que entonces las modificaciones del tipo no afectar�an al m�todo.

Eso, los TDUs no pueden hacerlo. Pero las clases, s�. Vamos a ver como:

El primer paso va a ser sustituir el tipo p�blico TipoAmigo por una clase p�blica clsAmigo. Tendr� las mismas variables. En alg�n caso (cuando no requieren "procesado, como es el caso del nombre) ser� como variables p�blicas. En otros casos (como con la fecha de nacimiento) ser� como propiedades, para poder incorporar "filtros" para los datos introducidos. En lugar de tener una propiedad "Edad", tendr� una propiedad p�blica "FechaNacimiento" y otra, de s�lo lectura, "EdadActual" (en este caso se ha a�adido un procedimieto Property Let que genera un error interceptable; si simplemente no se hubiera inclu�do el procediiento, se producir�a un error al compilar el m�dulo).

La clase se ha construido mediante el asistente de VB5 "Class Builder Utility", aunque posteriormente se ha modificado el nombre de las variables, y se ha modificado el c�digo

Adem�s, la propia clase tendr� sus reglas de validaci�n, que en este caso ser� s�lo para la fecha de nacimiento: los l�mites de valores, los valores por defecto... Est�n como constantes, pero se podr�an haber escrito como propiedades (o como propiedades ocultas, como luego veremos).

Lo m�s �til es que la clase no s�lo validar� sus datos y se inicializar�, sino que tambi�n la podremos pasar como un argumento, para poder establecer una comunicaci�n "privada" entre formularios. Esta comunicaci�n, adem�s, ser� independiente de la estructura interna de la clase clsAmigo (podemos a�adir propiedades, etc., sin tener que modificar los argumentos del m�dulo).

Cuando se vaya a a�adir o modificar un amigo, se env�a al formulario frmAmigos una copia de la clase clsAmigo. Puede ser ya una copia "rellena" (si se van a modificar los datos) o una copia en blanco (si se va a a�adir). Posteriormente, frmAmigos introduce los datos en la clase. La clase se "encarga" de avisar si los datos son incorrectos. Si todo va bien, frmAmigos informar� a frmGesti�n que no hay errores, y los datos se guardar�n.

La ventaja de pasar los datos por par�metros, en lugar de usar variables p�blicas, es que ya no hay valores "expuestos" que se puedan modificar por error. Los dos formularios y la clase se comunican con m�todos p�blicos, no mediante "atajos" como variables globales. Incluso hay mecanismos de seguridad (en frmAmigos) para evitar que pueda cargarse el formulario de forma an�mala (mediante .Show, sin pasar por su m�todo "PonerDatos". Quedan variables "indicadores de estado", pero ahora est�n dentro de los m�dulos, ocultas al resto del proyecto.

Como ahora cada m�dulo es independiente del funcionamiento y la estructura interna delos otros, se facilita el desarrollar en grupo. Basta con ponerse de acuerdo con la interfaz de clsAmigos, los m�todos, y luego cada uno trabajar (y mejorar) por su cuenta. Si posteriormente el desarrollador de clsAmigos decide modificarlo (por ejemplo, a�adir una nueva propiedad "Telefono") no afectar� al funcionamiento de frmGestion y frmAmigos (aunque no ser�n capaces de utilizar el nuevo dato).

En resumen: las clases tienen varias ventajas sobre los TDUs, pero las principales son que pueden ser miembros p�blicos de un m�dulo, y que pueden encargarse del procesado interno de los datos.

Los proyectos Pamig0 y Pamig1 muestran el c�digo anterior (con TDUs y con clases).

Sin embargo, el programa a�n no funciona bien: sigue habiendo una variable p�blica: la matriz de datos (de amigos). Est� expuesta a modificaciones sin control desde otros puntos del programa. Se puede acceder a los elementos de la matriz alegremente, e incluso a�adir o eliminarlos. Tal vez requiramos la utilizaci�n de alg�n tipo de matriz inteligente, como veremos m�s adelante.


Pulsa este link para bajarte el c�digo de ejemplo: Clases1c.zip 30.8 KB

ir al índice

Pulsa este link para ir a la p�gina de clases en Visual Basic

Este otro te llevar� al la segunda entrega